#include "engine.hpp"
#include "ticker.hpp"
#include "sdltool.hpp"
#include "cmdline_parser.hpp"
#include "gui.hpp"
#include <SDL_platform.h>

namespace sdl_engine {

Engine engine;

void Engine::Init(int argc, char** argv,
                  const Window::CreateInfo& windowCreateInfo,
                  const Renderer::CreateInfo& rendererCreateInfo,
                  bool canvaResizable,
                  u32 initFlag,
                  OpenGLInitInfo* openglInitInfo,
                  AndroidInitInfo* androidInitInfo) {
    log::Init(argc, argv);
    Logi("create cout log successed");

    if (SDL_Init(initFlag) < 0)
        throw SDLRuntimeError("SDL init failed");
    Logi("SDL init OK");

    if (openglInitInfo) {
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION,
                            openglInitInfo->majorVersion);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION,
                            openglInitInfo->minorVersion);
        switch (openglInitInfo->profile) {
            case OpenGLInitInfo::Profile::CORE:
                SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
                                    SDL_GL_CONTEXT_PROFILE_CORE);
                break;
            case OpenGLInitInfo::Profile::ES:
                SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
                                    SDL_GL_CONTEXT_PROFILE_ES);
                break;
            case OpenGLInitInfo::Profile::COMPATIBILITY:
                SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
                                    SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
                break;
        }
    }

#ifdef COMPILE_ON_ANDROID
    if (androidInitInfo) {
        if (androidInitInfo->seperateMouseAndTouch) {
            SDL_SetHint("SDL_HINT_ANDROID_SEPARATE_MOUSE_AND_TOUCH", "1");
        }
        if (androidInitInfo->mainVersion) {
            SDL_SetHint("SDL_HINT_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION",
                        std::to_string(androidInitInfo->mainVersion.value()).c_str());
        }
        if (androidInitInfo->patchVersion) {
            SDL_SetHint("SDL_HINT_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION",
                        std::to_string(androidInitInfo->patchVersion.value()).c_str());
        }
        if (!androidInitInfo->orientations.empty()) {
            std::string value;
            for (auto it = androidInitInfo->orientations.begin();
                 it != androidInitInfo->orientations.end();
                 it++) {
                switch (*it) {
                    case AndroidInitInfo::LANDSCAPE_LEFT:
                        value += "LandscapeLeft ";
                        break;
                    case AndroidInitInfo::LANDSCAPE_RIGHT:
                        value += "LandscapeRight ";
                        break;
                    case AndroidInitInfo::PORTRAIT:
                        value += "Portrait ";
                        break;
                    case AndroidInitInfo::PORTRAIT_UPSIDEDOWN:
                        value += "PortraitUpsideDown ";
                        break;
                }
            }
            if (!value.empty())
                value.pop_back();
            SDL_SetHint("SDL_HINT_ORIENTATIONS", value.c_str());
        }
    }
#endif

    if (IMG_Init(IMG_INIT_JPG|IMG_INIT_PNG) == 0)
        throw SDLImageRuntimeError("SDL_image init failed");
    Logi("SDL_image init OK");

    if (TTF_Init() == -1)
        throw SDLTTFRuntimeError("SDL_ttf init failed");
    Logi("SDL_ttf init OK");

    if (!(window_ = std::make_unique<Window>(windowCreateInfo)))
        throw SDLRuntimeError("Window create failed!");
    Logi("window create successed");

    if (openglInitInfo) {
        glContext_ = SDL_GL_CreateContext(window_->Raw());
        if (!glContext_) {
            Loge("opengl context create failed!");
        }
    }

    if (!(renderer_ = std::make_unique<Renderer>(rendererCreateInfo)))
        throw SDLRuntimeError("Renderer create failed!");
    Logi("renderer create successed");

    if (!(innerBmpFont_ = std::make_unique<InnerBmpFont>())) {
        LogeWithSDLErr("InnerBmpFont create failed!");
    } else {
        Logi("innerBmpFont create successed");
    }

    if (canvaResizable) {
        canva_.reset(new Texture(SDL_PIXELFORMAT_RGBA8888,
                                 SDL_TEXTUREACCESS_TARGET,
                                 windowCreateInfo.size_));
        if (!canva_) {
            Loge("canva create failed");
        } else {
            Logi("canva create ok");
        }
    }

    Logi("Engine init OK!");
}

void Engine::Shutdown() {
    if (app_) {
        app_->Shutdown();
        Logi("app shutdown");
    }

    if (glContext_) {
        SDL_GL_DeleteContext(glContext_);
        Logi("opengl context destroyed");
    }

    Logi("application shutdown OK! shutdown game!");
    fontStock_.Clear();
    Logi("font stock cleared");
    renderer_.reset();
    Logi("renderer destroyed");
    window_.reset();
    Logi("window destroyed");
    TTF_Quit();
    Logi("SDL_ttf quit");
    IMG_Quit();
    Logi("SDL_image quit");
    SDL_Quit();
    Logi("SDL quit");

    Logi("Engine shutdown OK, bye!");
}

App* Engine::SetApp(App* app) {
    App* curApp = app_;
    app_ = app;
    if (curApp) {
        curApp->Shutdown();
        Logi("app shutdown");
    }
    if (app_) {
        app_->Init();
        Logi("app init ok");
    }
    return curApp;
}

void Engine::EventHandle() {
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        gui::EventHandle(event);
        switch (event.type) {
            case SDL_QUIT:
                if (app_) {
                    if (app_->OnQuit(EventQuit())) {
                        Exit();
                    }
                } else {
                    Exit();
                }
                break;
            case SDL_KEYDOWN:
                if (app_)
                    app_->OnKeyDown(EventKeyDown(event.key.keysym.sym));
                break;
            case SDL_KEYUP:
                if (app_)
                    app_->OnKeyUp(EventKeyUp(event.key.keysym.sym));
                break;
            case SDL_MOUSEBUTTONDOWN:
                if (app_) {
                    Point pos = {event.button.x, event.button.y};
                    if (engine.IsCanvaResizable()) {
                        pos = mapGlobalPoint2Canva(pos);
                    }
                    app_->OnMouseButtonDown(EventMouseButtonDown(pos,
                                                                 static_cast<MouseButton>(
                                                                     event.button.button)));

                }
                break;
            case SDL_MOUSEBUTTONUP:
                if (app_) {
                    Point pos = {event.button.x, event.button.y};
                    if (engine.IsCanvaResizable()) {
                        pos = mapGlobalPoint2Canva(pos);
                    }
                    app_->OnMouseButtonUp(
                        EventMouseButtonUp(pos,
                                           static_cast<MouseButton>(
                                               event.button.button)));
                }
                break;
            case SDL_MOUSEMOTION:
                if (app_) {
                    Point pos = {event.motion.x, event.motion.y};
                    engine.realMousePosition_ = pos;
                    if (engine.IsCanvaResizable()) {
                        pos = mapGlobalPoint2Canva(pos);
                    }
                    app_->OnMouseMotion(
                        EventMouseMotion(pos,
                                         point(event.motion.xrel,
                                               event.motion.yrel)));
                }
                break;
            case SDL_MOUSEWHEEL:
                if (app_) {
                    app_->OnMouseWheel(EventMouseWheel(event.wheel.y *
                                                       (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED ?  -1 : 1)));
                }
                break;
            case SDL_WINDOWEVENT:
                switch (event.window.event) {
                    case SDL_WINDOWEVENT_RESIZED:
                        if (app_)
                            app_->OnWindowResize(EventWindowResize(
                                                 size(event.window.data1,
                                                      event.window.data2)));
                        break;
                }
                break;
            case SDL_FINGERMOTION:
                if (app_) {
                    Point pos = point(event.tfinger.x * engine.GetCanvaSize().w,
                                      event.tfinger.y * engine.GetCanvaSize().h);
                    app_->OnFingerMotion(EventTouchFinger(pos,
                                                          {event.tfinger.dx, event.tfinger.dy},
                                                          event.tfinger.pressure));
                }
                break;
            case SDL_FINGERDOWN:
                if (app_) {
                    Point pos = point(event.tfinger.x * engine.GetCanvaSize().w,
                                      event.tfinger.y * engine.GetCanvaSize().h);
                    if (engine.IsCanvaResizable()) {
                        pos = mapGlobalPoint2Canva(pos);
                    }
                    app_->OnFingerDown(EventTouchFinger(pos,
                                                        {event.tfinger.dx, event.tfinger.dy},
                                                        event.tfinger.pressure));
                }
                break;
            case SDL_FINGERUP:
                if (app_) {
                    Point pos = point(event.tfinger.x * engine.GetCanvaSize().w,
                                      event.tfinger.y * engine.GetCanvaSize().h);
                    if (engine.IsCanvaResizable()) {
                        pos = mapGlobalPoint2Canva(pos);
                    }
                    app_->OnFingerUp(EventTouchFinger(pos,
                                                      {event.tfinger.dx, event.tfinger.dy},
                                                      event.tfinger.pressure));
                }
                break;
            case SDL_MULTIGESTURE:
                if (app_) {
                    Point pos = point(event.mgesture.x * engine.GetCanvaSize().w,
                                      event.mgesture.y * engine.GetCanvaSize().h);
                    if (engine.IsCanvaResizable()) {
                        pos = mapGlobalPoint2Canva(pos);
                    }
                    app_->OnMultiGesture(EventMultiGesture(event.mgesture.dTheta,
                                                           event.mgesture.dDist,
                                                           pos,
                                                           event.mgesture.numFingers));
                }
                break;
        }
    }
}

Point Engine::mapGlobalPoint2Canva(const Point& p) const {
    Point pos;
    pos.x = float(p.x) / engine.GetWindow().GetSize().w * engine.canva_->GetSize().w;
    pos.y = float(p.y) / engine.GetWindow().GetSize().h * engine.canva_->GetSize().h;
    return pos;
}

void Engine::MainLoop() {
    static int elapse = 0;
    Ticker ticker;

    EventHandle();
    Update(elapse);
    Render();

    if (GetFPS() != NoFPSLimit) {
        elapse = ticker.Elapse();
        int remainTime = 1000.0 / fps_ - elapse;
        if (elapse> 0 && elapse < remainTime) {
            SDL_Delay(remainTime);
        }
    }
    elapse = ticker.Elapse();

    realTimeFPS_ = Fps();
}

void Engine::renderFPS(int fps) {
    GetInnerBmpFont().Render(Fmt("fps: %d", fps),
                             20,
                             point(0, 0),
                             Color::White);
}

void Engine::Update(int milliseconds) {
    if (app_)
        app_->Update(milliseconds);
}

void Engine::Render() {
    renderer_->SetTarget2Default();
    renderer_->SetDrawColor(clearColor_);
    renderer_->Clear();

    if (app_)
        app_->Render(*renderer_);

    if (showFPS_) {
        renderFPS(realTimeFPS_);
    }

    if (IsCanvaResizable()) {
        renderer_->SetTarget(nullptr);
        renderer_->RenderCopy(canva_.get(), point(0, 0), window_->GetSize());
    }

    renderer_->RenderPresent();
}

}
